package logsys

import (
	"encoding/xml"
	"fmt"
	"gitee.com/linbq-b7/go-log/rotate"
	"os"
	"runtime"
	"strings"
	"time"

	"go.uber.org/zap"
	"go.uber.org/zap/zapcore"
)

var option = &optionConfig{
	enablePackage: false, // 是否分别输出包日志文件（默认不输出）
	enableFile:    false, // 是否分别输出文件日志文件（默认不输出）
	fileMap:       make(map[string]*zap.SugaredLogger),
	bConfig:       false,
	output:        record{},
	timeLayout:    "2006-01-02T15:04:05.000Z0700",
	config: &logConfig{
		FilePath:     "./log/",       // 文件路径
		FilePrefix:   "logs-",        // 文件名前缀
		FileSuffix:   "%Y-%m-%d.log", // 文件名后缀
		MaxAge:       10,             // 保存文件时间（保存近10天日志）
		RotationTime: 24,             // 切割文件时间间隔（按天切割）
		RotationSize: 10,             // 切割文件大小（10M）
		MultiFile:    nil,            // 指定级别多文件（默认单文件）可选参数：debug，info，warn，error（忽略大小写）
		Encoder:      "json",         // 日志格式（默认json）可选参数：console，json
	},
	callback: func(msg string) {},
}

type outputConfig struct {
	Paths  *[]string
	Name   string
	Config *logConfig
}

type logConfig struct {
	FilePath     string
	FilePrefix   string
	FileSuffix   string
	MaxAge       time.Duration
	RotationTime time.Duration
	RotationSize int64
	MultiFile    *[]string
	Encoder      string
}

type optionConfig struct {
	enablePackage bool
	enableFile    bool
	fileMap       map[string]*zap.SugaredLogger
	bConfig       bool
	output        record
	timeLayout    string
	config        *logConfig
	callback      func(msg string)
}

type record struct {
	Output []*outputConfig
}

func Init(xmlPath string) {
	err := option.readFromXMl(&xmlPath, &option.output)
	if err != nil {
		InitDefault()

		Error("日志初始化失败: %s", err.Error())
		os.Exit(1)

		return
	}

	option.bConfig = true
	option.getConfigLogger()
}

func InitDefault() {
	all := "all"
	option.getLogger(&all, option.config)
}

func SetCallback(callback func(msg string)) {
	option.callback = callback
}

func Debug(template string, args ...interface{}) {

	option.log(zapcore.DebugLevel, &template, args...)
}

func Info(template string, args ...interface{}) {

	option.log(zapcore.InfoLevel, &template, args...)
}

func Warn(template string, args ...interface{}) {

	option.log(zapcore.WarnLevel, &template, args...)
}

func Error(template string, args ...interface{}) {

	option.log(zapcore.ErrorLevel, &template, args...)
}

func Panic(template string, args ...interface{}) {

	option.log(zapcore.PanicLevel, &template, args...)
}

func Fatal(template string, args ...interface{}) {

	option.log(zapcore.FatalLevel, &template, args...)
}

func (opCfg *optionConfig) readFromXMl(filename *string, key interface{}) error {
	in, err := os.Open(*filename)
	if err != nil {
		return err
	}
	decodeXML := xml.NewDecoder(in)
	err = decodeXML.Decode(key)
	err = in.Close()
	if err != nil {
		return err
	}
	return err
}

func (opCfg *optionConfig) log(level zapcore.Level, template *string, args ...interface{}) {
	pkg, fileLine := opCfg.getPackage()
	msg := fmt.Sprintf(*opCfg.templateMsg(fileLine, template), args...)
	fmt.Printf(fmt.Sprintf("%s\t%s\t%s\n",
		time.Now().Format(option.timeLayout),
		strings.ToUpper(level.String()),
		msg))

	if option.bConfig {
		opCfg.logWithConfig(level, fileLine, &msg)
	} else {
		all := "all"
		opCfg.write(opCfg.getLogger(&all, option.config), level, &msg)
	}
	if pkg != nil {
		opCfg.write(opCfg.getLogger(pkg, option.config), level, &msg)
	}
	file := opCfg.getFile(fileLine)
	if file != nil {
		opCfg.write(opCfg.getLogger(file, option.config), level, &msg)
	}

	if opCfg.callback != nil {
		opCfg.callback(msg)
	}
}

func (opCfg *optionConfig) logWithConfig(
	level zapcore.Level,
	fileLine *string,
	msg *string) {

	if fileLine == nil {
		return
	}
	for _, out := range option.output.Output {

		if opCfg.checkOutput(out, false) {
			continue
		}
		opCfg.checkConfig(out)

		logger := opCfg.getLogger(&out.Name, out.Config)
		for _, v := range *out.Paths {
			if strings.HasPrefix(*fileLine, v) {
				opCfg.write(logger, level, msg)
				break
			}
		}
	}
}

func (opCfg *optionConfig) checkOutput(out *outputConfig, init bool) bool {
	ret := false
	if len(out.Name) == 0 || out.Paths == nil || len(*out.Paths) == 0 {
		ret = true
	}
	if init && !ret {
		toPaths := make([]string, 0)
		for _, path := range *out.Paths {
			path = strings.TrimLeft(strings.TrimRight(path, " "), " ")
			if len(path) > 0 {
				toPaths = append(toPaths, path)
			}
		}
		out.Paths = &toPaths
		ret = len(*out.Paths) == 0
	}
	return ret
}

func (opCfg *optionConfig) getConfigLogger() {

	for _, out := range option.output.Output {

		if opCfg.checkOutput(out, true) {
			continue
		}
		opCfg.checkConfig(out)
		opCfg.getLogger(&out.Name, out.Config)
	}
}

func (opCfg *optionConfig) checkConfig(cfg *outputConfig) {
	if cfg.Config == nil {
		cfg.Config = option.config
	} else {
		if cfg.Config.FilePath == "" {
			cfg.Config.FilePath = option.config.FilePath
		}
		if cfg.Config.FilePrefix == "" {
			cfg.Config.FilePrefix = option.config.FilePrefix
		}
		if cfg.Config.FileSuffix == "" {
			cfg.Config.FileSuffix = option.config.FileSuffix
		}
		if cfg.Config.MaxAge == 0 {
			cfg.Config.MaxAge = option.config.MaxAge
		}
		if cfg.Config.RotationTime == 0 {
			cfg.Config.RotationTime = option.config.RotationTime
		}
		if cfg.Config.RotationSize == 0 {
			cfg.Config.RotationSize = option.config.RotationSize
		}
	}
}

func (opCfg *optionConfig) write(
	logger *zap.SugaredLogger,
	level zapcore.Level,
	msg *string) {

	switch level {
	case zapcore.DebugLevel:
		logger.Debugf(*msg)
	case zapcore.InfoLevel:
		logger.Infof(*msg)
	case zapcore.WarnLevel:
		logger.Warnf(*msg)
	case zapcore.ErrorLevel:
		logger.Errorf(*msg)
	case zapcore.PanicLevel:
		logger.DPanicf(*msg)
	case zapcore.FatalLevel:
		logger.Fatalf(*msg)
	}
}

func (opCfg *optionConfig) templateMsg( /*fileLine *string*/ _ *string, template *string) *string {
	return template
	//var ret *string = template
	//if fileLine != nil {
	//	tmp := fmt.Sprintf("%s	%s", *fileLine, *template)
	//	ret = &tmp
	//}
	//return ret
}

func (opCfg *optionConfig) getLogger(pkg *string, config *logConfig) *zap.SugaredLogger {
	file, ok := option.fileMap[*pkg]
	if !ok {
		file = opCfg.initLogger(pkg, config)
		option.fileMap[*pkg] = file
	}
	return file
}

func (opCfg *optionConfig) initLogger(pkg *string, config *logConfig) *zap.SugaredLogger {
	encoderConfig := zap.NewProductionEncoderConfig()
	encoderConfig.EncodeTime = zapcore.TimeEncoderOfLayout(option.timeLayout) //ISO8601TimeEncoder
	encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder
	encoderConfig.TimeKey = "time"

	// 日志输出格式
	var encoder zapcore.Encoder
	if config.Encoder == "console" {
		encoder = zapcore.NewConsoleEncoder(encoderConfig)
	} else {
		encoder = zapcore.NewJSONEncoder(encoderConfig)
	}

	toCores := make([]zapcore.Core, 0)
	multiFile, levels := opCfg.checkMultiFile(config.MultiFile)
	for _, level := range *levels {
		toCores = append(toCores, *opCfg.newCore(&encoder, level, pkg, config, multiFile))
	}

	return zap.New(
		zapcore.NewTee(toCores...),
		//zap.AddCaller(),
		//zap.AddCallerSkip(4),
		//zap.AddStacktrace(ErrorLevel),
	).Sugar()
}

func (opCfg *optionConfig) checkMultiFile(multiFile *[]string) (bool, *[]zapcore.Level) {
	ret := true
	toLevels := make([]zapcore.Level, 0)
	if multiFile == nil || len(*multiFile) == 0 {
		ret = false
	}
	if ret {
		for _, v := range *multiFile {
			v = strings.TrimLeft(strings.TrimRight(v, " "), " ")
			if len(v) > 0 {
				level, err := zapcore.ParseLevel(v)
				if err == nil {
					toLevels = append(toLevels, level)
				}
			}
		}
		ret = len(toLevels) > 0
	}
	if !ret {
		toLevels = append(toLevels, zapcore.DebugLevel)
	}
	return ret, &toLevels
}

func (opCfg *optionConfig) newCore(
	enc *zapcore.Encoder,
	level zapcore.Level,
	pkg *string,
	config *logConfig,
	multiFile bool) *zapcore.Core {

	en := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool {
		enable := lvl >= level
		if multiFile {
			enable = lvl == level
		}
		return enable
	})
	filePath := config.FilePath + *pkg + "/" + config.FilePrefix
	if multiFile {
		filePath += level.String() + "-"
	}
	ws, err := opCfg.getLogWriter(&filePath, config)
	if err != nil {
		return nil
	}
	core := zapcore.NewCore(*enc, *ws, en)
	return &core
}

func (opCfg *optionConfig) getLogWriter(filePath *string, config *logConfig) (*zapcore.WriteSyncer, error) {
	writer, err := rotate.New(
		*filePath+config.FileSuffix,
		rotate.WithMaxAge(config.MaxAge*time.Hour*24),
		rotate.WithRotationTime(config.RotationTime*time.Hour),
		rotate.WithRotationSize(config.RotationSize*1024*1024),
	)
	if err != nil {
		return nil, err
	}
	err = writer.Rotate()
	if err != nil {
		return nil, err
	}
	syncer := zapcore.AddSync(writer)
	return &syncer, nil
}

func (opCfg *optionConfig) getPackage() (*string, *string) {
	pkg, fileLine := opCfg.getFileLine()
	if !option.enablePackage {
		return nil, fileLine
	}
	return pkg, fileLine
}

func (opCfg *optionConfig) getFile(fileLine *string) *string {
	var file *string = nil
	if !option.enableFile || fileLine == nil {
		return file
	}
	index := strings.LastIndex(*fileLine, ".")
	if index > 0 {
		tmp := (*fileLine)[:index]
		file = &tmp
	}
	return file
}

func (opCfg *optionConfig) getFileLine() (*string, *string) {
	var pkg *string = nil
	var fileLine *string = nil
	pc, file, line, ok := runtime.Caller(4)
	if !ok {
		return pkg, fileLine
	}
	name := runtime.FuncForPC(pc).Name()
	index := strings.LastIndex(name, ".(")
	if index > 0 {
		tmp := name[:index]
		pkg = &tmp
	} else {
		// fix app/action/kick.handler.func1 取第一个进行拼接
		index = strings.Index(name, ".")
		if index > 0 {
			tmp := name[:index]
			pkg = &tmp
		}
	}
	index = strings.LastIndex(file, "/")
	if index > 0 {
		file = file[index:]
	}
	tmp := fmt.Sprintf("%s%s:%d", *pkg, file, line)
	fileLine = &tmp
	return pkg, fileLine
}
